/*
 * Copyright 2010 Gal Dolber.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.unnison.framework.rebind.binder;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JAnnotationType;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JField;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JPackage;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.event.dom.client.HasNativeEvent;
import com.google.gwt.event.shared.EventHandler;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import com.google.gwt.user.rebind.StringSourceWriter;

import com.unnison.framework.client.GuitEntryPoint;
import com.unnison.framework.client.Implementation;
import com.unnison.framework.client.ImplicitCast;
import com.unnison.framework.client.Presenter;
import com.unnison.framework.client.View;
import com.unnison.framework.client.binder.Attribute;
import com.unnison.framework.client.binder.BinderContributorAnnotation;
import com.unnison.framework.client.binder.EventBusHandler;
import com.unnison.framework.client.binder.Provided;
import com.unnison.framework.client.binder.ViewAccesor;
import com.unnison.framework.client.binder.ViewField;
import com.unnison.framework.client.binder.ViewFields;
import com.unnison.framework.client.binder.ViewHandler;
import com.unnison.framework.client.binder.ViewInitializer;
import com.unnison.framework.client.binder.ViewPool;
import com.unnison.framework.client.eventbus.EventBus;
import com.unnison.framework.rebind.common.AbstractGenerator;
import com.unnison.framework.rebind.guitview.GuitViewGenerator;
import com.unnison.framework.rebind.guitview.GuitViewHelper;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;

public class GuitBinderGenerator extends AbstractGenerator {

    private final GuitViewGenerator guitViewGenerator = new GuitViewGenerator();
    private static final String STRINGCANONICALNAME = String.class.getCanonicalName();

    protected JClassType hasNativeEventType;

    protected JPackage domEventsPackage;
    protected JPackage sharedEventsPackage;
    protected JClassType gwtEventType;
    protected JClassType gwtHandlerType;

    protected static final String handlerRegistrationName = HandlerRegistration.class.getCanonicalName();

    protected static final String bindingsDeclaration = ArrayList.class.getCanonicalName() + "<" + handlerRegistrationName
        + "> bindings = new " + ArrayList.class.getCanonicalName() + "<" + handlerRegistrationName + ">();";

    protected static final String eventBusbindingsDeclaration = ArrayList.class.getCanonicalName() + "<" + handlerRegistrationName
        + "> eventBusBindings = new " + ArrayList.class.getCanonicalName() + "<" + handlerRegistrationName + ">();";

    protected HashMap<Class<? extends Annotation>, BinderContributor> contributors;

    protected String capitalize(String part) {
        return part.substring(0, 1).toUpperCase() + part.substring(1);
    }

    private boolean checkIsPresenter(JClassType clazz) throws UnableToCompleteException {
        if (clazz.isAssignableTo(getType(Presenter.class.getCanonicalName()))) {
            return true;
        }
        return false;
    }

    protected String eventClassNameToEventName(String simpleName) {
        String eventName;
        eventName = simpleName.substring(0, simpleName.length() - 5);
        eventName = eventName.substring(0, 1).toLowerCase() + eventName.substring(1);
        return eventName;
    }

    protected String eventNameToEventClassName(String eventName) {
        eventName = eventName.substring(0, 1).toUpperCase() + eventName.substring(1) + "Event";
        return eventName;
    }

    private void findAllMethods(JClassType type, ArrayList<JMethod> methods) {
        JMethod[] typeMethods = type.getMethods();
        methods.addAll(Arrays.asList(typeMethods));
        JClassType[] interfaces = type.getImplementedInterfaces();
        for (JClassType i : interfaces) {
            findAllMethods(i, methods);
        }
    }

    @SuppressWarnings("unchecked")
    protected void findContributors() throws UnableToCompleteException {
        if (contributors == null) {
            List<String> contributorsNames = getConfigurationProperty("app.binder.contributor").getValues();
            contributors = new HashMap<Class<? extends Annotation>, BinderContributor>();
            for (String c : contributorsNames) {
                JClassType cType = getType(c);
                if (cType == null) {
                    error("Error in binder contributor name. Found: %s", c);
                }

                JAnnotationType cAnnotation = cType.isAnnotation();
                if (cAnnotation == null) {
                    error("All binder contributors must be annotations. Found: %s", c);
                }

                if (!cAnnotation.isAnnotationPresent(BinderContributorAnnotation.class)) {
                    error("All binder contributors must be annotated with %s. Found: %s",
                        BinderContributorAnnotation.class.getCanonicalName(), c);
                }

                Class<?> binderContributor = cAnnotation.getAnnotation(BinderContributorAnnotation.class).value();
                if (!BinderContributor.class.isAssignableFrom(binderContributor)) {
                    error("All binder contributors must implement %s. Found: %s", BinderContributor.class.getCanonicalName(), c);
                }

                contributors.put((Class<? extends Annotation>) findClass(c), (BinderContributor) newInstance(binderContributor));
            }
        }
    }

    @Override
    protected void generate(SourceWriter writer) throws UnableToCompleteException {
        findContributors();

        hasNativeEventType = hasNativeEventType == null ? getType(HasNativeEvent.class.getCanonicalName()) : hasNativeEventType;

        // Do we need to check this?
        JClassType presenterType = baseClass.getImplementedInterfaces()[0].isParameterized().getTypeArgs()[0];

        // If it is a parameterized type we need to find the base type to get the right method names
        if (presenterType.isParameterized() != null) {
            presenterType = presenterType.isParameterized().getBaseType();
        }

        // Presenter or controller
        boolean isPresenter = checkIsPresenter(presenterType);

        writer.println(bindingsDeclaration);
        writer.println(eventBusbindingsDeclaration);

        String viewTypeName = null;
        HashMap<String, JType> validBindingFieldsTypes = null;
        if (isPresenter) {
            viewTypeName = guitViewGenerator.generate(logger, context, presenterType.getQualifiedSourceName());
            writer.println(viewTypeName + " view;");

            // Field -> Type (for validation purpose)
            validBindingFieldsTypes = getValidGuitViewBindingFields(presenterType);

            // View pool
            writer.println("private static " + ViewPool.class.getCanonicalName() + " pool = new " + ViewPool.class.getCanonicalName()
                + "();");
        }

        writer.println(presenterType.getQualifiedSourceName() + " presenter;");

        writer.println(EventBus.class.getCanonicalName() + " eventBus;");

        writer.println("boolean isBinded = false;");

        /*
         * Event bus binder.
         */
        writer.println("public void bind(final " + presenterType.getQualifiedSourceName() + " presenter, final "
            + EventBus.class.getCanonicalName() + " eventBus) {");
        writer.indent();

        writer.println("this.presenter = presenter;");
        writer.println("this.eventBus = eventBus;");
        printEventBusBindingMethods(writer, presenterType);

        writer.outdent();
        writer.println("}");

        /*
         * View binder.
         */
        writer.println("public " + View.class.getCanonicalName() + " getView() {");
        writer.indent();

        if (isPresenter) {
            // If already binded return the view
            writer.println("if (isBinded) { return view; }");

            // Find a view from the pool or create a new one
            writer.println("view = (" + viewTypeName + ")(pool.isEmpty() ? new " + viewTypeName + "() : pool.pop());");
            
            // Provided fields
            printProvidedFields(presenterType, validBindingFieldsTypes, writer);
            
            writer.println("view.bind();");
            printViewFieldBindings(writer, presenterType, viewTypeName, validBindingFieldsTypes);
            printViewBindingMethods(writer, presenterType, viewTypeName, validBindingFieldsTypes.keySet());

            // Initializers
            printPresentersInitilizersCalls(writer, presenterType);

            writer.println("isBinded = true;");

            // Return the view or null
            writer.println("return view;");
        } else {
            writer.println("return null;");
        }

        writer.outdent();
        writer.println("}");

        /*
         * Unbind
         */
        writer.println("public void releaseView() {");
        writer.indent();

        if (isPresenter) {
            writer.println("for (" + handlerRegistrationName + " b : bindings) {b.removeHandler();}");
            writer.println("bindings.clear();");
            if (isPresenter) {
                // Recycle the view
                writer.println("if (view != null) {");
                writer.indent();
                writer.println("pool.push(view);");
                writer.println("view = null;");
                writer.outdent();
                writer.println("}");
            }
            writer.println("isBinded = false;");
        }
        writer.outdent();
        writer.println("}");

        /*
         * Destroy.
         */
        writer.println("public void destroy() {");
        writer.indent();
        writer.println("for (" + handlerRegistrationName + " b : eventBusBindings) {b.removeHandler();}");
        if (isPresenter) {
            writer.println("releaseView();");
        }
        writer.println("bindings = null;");
        writer.println("eventBusBindings = null;");
        writer.println("presenter = null;");
        writer.println("eventBus = null;");
        writer.outdent();
        writer.println("}");
    }

    private void printProvidedFields(JClassType baseClass, HashMap<String, JType> validBindingFieldsTypes, SourceWriter writer) throws UnableToCompleteException {
        JField[] fields = baseClass.getFields();
        for (JField f : fields) {
            if (f.isAnnotationPresent(ViewField.class)) {
                String name = f.getAnnotation(ViewField.class).value();
                if (name.isEmpty()) {
                    name = f.getName();
                }
                
                if (f.isAnnotationPresent(Provided.class)) {
                    writer.println("view." + name + " = (" + validBindingFieldsTypes.get(name).getQualifiedSourceName() + ")presenter." + f.getName() + ";");
                }
            }
        }
    }
    
    private String getAddMethodName(String eventName) {
        return "add" + eventName.substring(0, 1).toUpperCase() + eventName.substring(1) + "Handler";
    }

    /**
     * Find event in dom, shared or context locations. Context = '{currentPackage}/event'. Context events have priority
     * over shared and dom ones.
     */
    protected JClassType getEventByName(String eventName, JPackage contextEventsPackage) {
        // Get event class name
        eventName = eventNameToEventClassName(eventName);

        // Find in context
        if (contextEventsPackage != null) {
            JClassType contextEventType = contextEventsPackage.findType(eventName);
            if (contextEventType != null) {
                return contextEventType;
            }
        }

        // Find in dom events
        JClassType domEventType = domEventsPackage.findType(eventName);
        if (domEventType != null) {
            return domEventType;
        }

        // Find in shared events
        JClassType sharedEventType = sharedEventsPackage.findType(eventName);
        if (sharedEventType != null) {
            return sharedEventType;
        }

        return null;
    }

    /**
     * Retrieves the handler associated with the event.
     * @throws UnableToCompleteException
     */
    protected JClassType getHandlerForEvent(JClassType eventType) throws UnableToCompleteException {
        return eventType.findMethod("getAssociatedType", new JType[0]).getReturnType().isParameterized().getTypeArgs()[0];
    }

    private HashMap<String, JType> getValidGuitViewBindingFields(JClassType presenterType) throws UnableToCompleteException {
        Set<String> findUiFields = GuitViewHelper.findUiFields(presenterType, logger, false);

        // TODO Improve this
        HashMap<String, JType> fields = new HashMap<String, JType>();
        for (String f : findUiFields) {
            String[] parts = f.split(" ");

            // Parameterized?
            String type = parts[1];
            int index = type.indexOf("<");
            if (index != (-1)) {
                type = type.substring(0, index);
            }

            String name = parts[2].substring(0, parts[2].length() - 1);
            
            JClassType fieldType = getType(type);
            if (fieldType == null) {
                error("The type '%s' of the field '%s' doesn't exists. Found: %s.ui.xml", type, name, presenterType.getQualifiedSourceName());
            }
            
            fields.put(name, fieldType);
        }
        return fields;
    }

    /**
     * Finds the valid methods and check all the conventions.
     */
    protected void printEventBusBindingMethods(SourceWriter writer, JClassType presenterType) throws UnableToCompleteException {
        JPackage contextEventsPackage = getPackage(presenterType.getPackage().getName() + ".event");
        for (JMethod m : presenterType.getMethods()) {
            if (m.isAnnotationPresent(EventBusHandler.class)) {

                EventBusHandler eventBusHandler = m.getAnnotation(EventBusHandler.class);

                String name = m.getName();
                String presenterName = presenterType.getQualifiedSourceName();

                validateHandler(m, name, presenterName);

                // Find the event type
                JParameter[] parameters = m.getParameters();

                if (!name.startsWith("$") && !name.startsWith("eventBus$")) {
                    error("Bad method name: %s on class: %s, the method should start with '$' or 'eventBus$'", name, presenterName);
                }

                // Clean the name
                if (name.startsWith("$")) {
                    name = name.substring(1); // Cut off the $
                } else {
                    name = name.substring(9); // Cut off the eventBus$
                }

                JClassType eventType = getType(eventBusHandler.value().getCanonicalName());
                if (eventType.equals(gwtEventType)) {
                    eventType = getEventByName(name, contextEventsPackage);
                    if (eventType == null) {
                        error("There is no context, dom or shared event with the name '%s'. Binding method: '%s' in class: '%s'", name,
                            m.getName(), presenterType.getQualifiedSourceName());
                    }
                }

                StringBuilder bindingParameters = new StringBuilder();
                ArrayList<String> parameterStrings = new ArrayList<String>();
                for (JParameter p : parameters) {
                    String parameter = p.getName();

                    if (bindingParameters.length() > 0) {
                        bindingParameters.append(", ");
                    }

                    int initlenght = bindingParameters.length();

                    // Implicit cast
                    bindingParameters.append("(" + p.getType().getErasedType().getParameterizedQualifiedSourceName() + ")");

                    String getter = "get";

                    // Check if it is a boolean then the getter is 'is' not 'get'
                    JPrimitiveType parameterTypeIsPrimitive = p.getType().isPrimitive();
                    if (parameterTypeIsPrimitive != null && parameterTypeIsPrimitive.equals(JPrimitiveType.BOOLEAN)) {
                        getter = "is";
                    }

                    // Event getter binding
                    if (parameter.indexOf("$") == -1) {
                        // Event binding
                        bindingParameters.append("event.");
                        bindingParameters.append(getter);
                        bindingParameters.append(capitalize(parameter));
                        bindingParameters.append("()");
                    } else {
                        // Event binding anidated binding
                        String[] parameterParts = parameter.split("[$]");

                        bindingParameters.append("event");
                        for (int n = 0; n < parameterParts.length - 1; n++) {
                            bindingParameters.append(".get");
                            bindingParameters.append(capitalize(parameterParts[n]));
                            bindingParameters.append("()");
                        }

                        bindingParameters.append(".");
                        bindingParameters.append(getter);
                        bindingParameters.append(capitalize(parameterParts[parameterParts.length - 1]));
                        bindingParameters.append("()");
                    }

                    parameterStrings.add(bindingParameters.substring(initlenght, bindingParameters.length()));
                }

                // Find the event name
                String simpleName = eventType.getSimpleSourceName();
                if (!simpleName.endsWith("Event")) {
                    error("The event %s does not use the event convention. It should end with 'Event'", eventType.getQualifiedSourceName());
                }
                String eventName = eventClassNameToEventName(simpleName);

                // Check that the name of the event correspond to the method name convention
                if (!eventName.equals(name)) {
                    error("The method %s on class %s does not use the event bus handler method convention. "
                        + "It should start with '$' or 'eventBus$' "
                        + "and end with the event name. i.e ValueChangeEvent -> $valueChange. Solve it renaming it to '$%s'", m.getName(),
                        presenterName, eventName);
                }

                // Get event handler name
                JClassType handlerType = getHandlerForEvent(eventType);
                if (handlerType == null) {
                    error("Parameter '%s' is not an event (subclass of GwtEvent).", eventType.getName());
                }

                // Retrieves the single method (usually 'onSomething') related to all
                // handlers. Ex: onClick in ClickHandler, onBlur in BlurHandler ...
                JMethod[] methods = handlerType.getMethods();
                if (methods.length != 1) {
                    error("'%s' has more than one method defined.", handlerType.getName());
                }

                // 'onSomething' method
                JMethod handlerOnMethod = methods[0];

                // Checks if the method has an Event as parameter. Ex: ClickEvent in onClick, BlurEvent in onBlur ...
                parameters = handlerOnMethod.getParameters();
                if (parameters.length != 1) {
                    error("Method '%s' needs '%s' as parameter", handlerOnMethod.getName(), eventType.getName());
                }

                writer.println("eventBusBindings.add(eventBus.addHandler(new " + handlerType.getQualifiedSourceName() + "() {");
                writer.indent();
                writer.println("public void " + handlerOnMethod.getName() + "(final " + eventType.getQualifiedSourceName() + " event) {");
                writer.indent();

                // Process contributors
                String bindingParametersString = bindingParameters.toString();
                BinderContextImpl binderContext = processMethodContributors(presenterType, null, null, null, m, eventType,
                    parameterStrings.toArray(new String[parameterStrings.size()]));

                StringSourceWriter handlerWriter = new StringSourceWriter();

                handlerWriter.println("if (!GWT.isScript()) {");
                handlerWriter.println(GuitEntryPoint.class.getCanonicalName() + ".getLogger().initLog(\"EVENTBUS " + binderContext.getLog()
                    + "\");");
                handlerWriter.println("}");

                handlerWriter.println("presenter." + m.getName() + "(" + bindingParametersString + ");");

                handlerWriter.println("if (!GWT.isScript()) {");
                handlerWriter.println(GuitEntryPoint.class.getCanonicalName() + ".getLogger().endLog();");
                handlerWriter.println("}");

                writer.println(binderContext.build(handlerWriter));

                writer.outdent();
                writer.println("}");
                writer.outdent();
                writer.println("}, " + eventType.getQualifiedSourceName() + ".getType()));");
            }
        }
    }

    /**
     * Print calls to @PresenterInitializer methods.
     */
    protected void printPresentersInitilizersCalls(SourceWriter writer, JClassType presenterType) throws UnableToCompleteException {
        for (JMethod m : presenterType.getMethods()) {
            if (m.isAnnotationPresent(ViewInitializer.class)) {
                if (m.getParameters().length > 0) {
                    error("All @PresenterInitializer must have zero parameters. Found: %s.%s", presenterType.getQualifiedSourceName(),
                        m.getName());
                }

                writer.println("presenter." + m.getName() + "();");
            }
        }

        JClassType superclass = presenterType.getSuperclass();
        if (superclass != null) {
            printPresentersInitilizersCalls(writer, superclass);
        }
    }

    private void printViewBindingMethods(SourceWriter writer, JClassType presenterType, String viewTypeName, Set<String> validBindingFields)
        throws UnableToCompleteException {

        JPackage contextEventsPackage = getPackage(presenterType.getPackage().getName() + ".event");
        for (JMethod m : presenterType.getMethods()) {
            if (m.isAnnotationPresent(ViewHandler.class)) {
                String name = m.getName();

                validateHandler(m, name, presenterType.getQualifiedSourceName());

                String eventName;
                JClassType eventType = getType(m.getAnnotation(ViewHandler.class).value().getCanonicalName());

                Set<String> bindingFields = null;
                if (!m.isAnnotationPresent(ViewFields.class)) {
                    if (name.startsWith("$")) {
                        // Direct view binding
                        eventName = name.substring(1);
                    } else {
                        // View fields binding
                        String[] nameParts = name.split("[$]");

                        // Check the name format
                        if (nameParts.length < 2) {
                            error("The method %s on the class %s have a bad binding format. It should be: "
                                + "'{viewField}${eventName}' or for binding multiple fields: '{viewField1}${viewField2}${eventName}'",
                                name, presenterType.getQualifiedSourceName());
                        }

                        // Check that the binding fields are valid
                        bindingFields = new HashSet<String>();
                        for (int n = 0; n < nameParts.length - 1; n++) {
                            if (!validBindingFields.contains(nameParts[n])) {
                                error(
                                    "The field %s on the class %s is not a valid binding field. It must be public or protected and not static",
                                    nameParts[n], viewTypeName);
                            }
                            bindingFields.add(nameParts[n]);
                        }
                        eventName = nameParts[nameParts.length - 1]; // last token
                    }

                    // Check the event type and name convention
                    if (eventType.equals(gwtEventType)) {
                        eventType = getEventByName(eventName, contextEventsPackage);
                        if (eventType == null) {
                            error("There is no context, dom or shared event with the name '%s'. Binding method: '%s' in class: '%s'",
                                eventName, name, presenterType.getQualifiedSourceName());
                        }
                    } else {
                        // Check that the method name correspond to the event type
                        String eventNameToEventClassName = eventNameToEventClassName(eventName);
                        if (!eventNameToEventClassName.equals(eventType.getSimpleSourceName())) {
                            error("The method '%s' in the class '%s' have a typo in the name. The last token should be : ..$%s() {.. ",
                                name, presenterType.getQualifiedSourceName(), eventName);
                        }
                    }
                } else {
                    String[] fields = m.getAnnotation(ViewFields.class).value();
                    bindingFields = new HashSet<String>();
                    for (String f : fields) {
                        if (!validBindingFields.contains(f)) {
                            error(
                                "The field %s on the class %s is not a valid binding field. It must be public or protected and not static",
                                f, viewTypeName);
                        }
                        bindingFields.add(f);
                    }

                    if (eventType.equals(gwtEventType)) {
                        error("When using ViewFields you must specify the event class in the Handler annotation. Found: %s.%s",
                            presenterType.getQualifiedSourceName(), m.getName());
                    }

                    eventName = eventClassNameToEventName(eventType.getSimpleSourceName());
                }

                /**
                 * Find parameters bindings. The binding can be with the event(cannot have anidation of
                 * getters):'getter'->'getGetter()' or with the view:'$getter'->'view.getGetter()' or with a view field
                 * '{viewField$getter}'->'view.viewField.getGetter();', this last two ones will support anidation:
                 * '{viewField$getter$another}'->'view.viewField.getGetter().getAnother();'
                 **/
                StringBuilder bindingParameters = new StringBuilder();
                JParameter[] parameters = m.getParameters();
                ArrayList<String> parameterStrings = new ArrayList<String>();
                for (JParameter p : parameters) {
                    String parameter = p.getName();
                    JType parameterType = p.getType();
                    if (bindingParameters.length() > 0) {
                        bindingParameters.append(", ");
                    }

                    int initlenght = bindingParameters.length();

                    // Implicit cast
                    bindingParameters.append("(" + parameterType.getErasedType().getParameterizedQualifiedSourceName() + ")");

                    String getter = "get";

                    // Check if it is a boolean then the getter is 'is' not 'get'
                    JPrimitiveType parameterTypeIsPrimitive = parameterType.isPrimitive();
                    if (parameterTypeIsPrimitive != null && parameterTypeIsPrimitive.equals(JPrimitiveType.BOOLEAN)) {
                        getter = "is";
                    }

                    // TODO Validate this? Compiler error logs are pretty clear.
                    if (p.isAnnotationPresent(Attribute.class)) {
                        // Only valid for domEvents
                        if (!eventType.isAssignableTo(hasNativeEventType)) {
                            error("Attributes binding are only valid for DomEvents. Found: %s.%s in parameter: %s",
                                presenterType.getQualifiedSourceName(), name, parameter);
                        }

                        String parameterTypeQualifiedSourceName = parameterType.getQualifiedSourceName();
                        boolean isString = parameterTypeQualifiedSourceName.equals(STRINGCANONICALNAME);
                        if (!isString) {
                            bindingParameters.append(parameterTypeQualifiedSourceName + ".valueOf(");
                        }
                        bindingParameters.append("((" + Element.class.getCanonicalName()
                            + ")event.getNativeEvent().getEventTarget().cast()).getAttribute(\"" + parameter + "\")");
                        if (!isString) {
                            bindingParameters.append(")");
                        }
                    } else if (parameter.indexOf("$") == -1) {
                        // Event binding
                        bindingParameters.append("event.");
                        bindingParameters.append(getter);
                        bindingParameters.append(capitalize(parameter));
                        bindingParameters.append("()");
                    } else {
                        // Event binding anidated binding
                        String[] parameterParts = parameter.split("[$]");

                        bindingParameters.append("event");
                        for (int n = 0; n < parameterParts.length - 1; n++) {
                            bindingParameters.append(".get");
                            bindingParameters.append(capitalize(parameterParts[n]));
                            bindingParameters.append("()");
                        }

                        bindingParameters.append(".");
                        bindingParameters.append(getter);
                        bindingParameters.append(capitalize(parameterParts[parameterParts.length - 1]));
                        bindingParameters.append("()");
                    }

                    parameterStrings.add(bindingParameters.substring(initlenght, bindingParameters.length()));
                }

                // Get event handler name
                JClassType handlerType = getHandlerForEvent(eventType);
                if (handlerType == null) {
                    error("Parameter '%s' is not an event (subclass of GwtEvent).", eventType.getName());
                }

                // Retrieves the single method (usually 'onSomething') related to all
                // handlers. Ex: onClick in ClickHandler, onBlur in BlurHandler ...
                JMethod[] methods = handlerType.getMethods();
                if (methods.length != 1) {
                    error("'%s' has more than one method defined.", handlerType.getName());
                }

                // 'onSomething' method
                JMethod handlerOnMethod = methods[0];

                String methodName = m.getName();
                String handlerTypeName = handlerType.getQualifiedSourceName();

                // Write handler
                SourceWriter eventHandlerWriter = new StringSourceWriter();
                eventHandlerWriter.println("new " + handlerTypeName + "() {");
                eventHandlerWriter.indent();
                eventHandlerWriter.println("public void " + handlerOnMethod.getName() + "(final " + eventType.getQualifiedSourceName()
                    + " event) {");
                eventHandlerWriter.indent();

                String bindingParametersString = bindingParameters.toString();

                // Process contributors
                BinderContextImpl binderContext = processMethodContributors(presenterType, null, null, viewTypeName, m, eventType,
                    parameterStrings.toArray(new String[parameterStrings.size()]));

                StringSourceWriter handlerWriter = new StringSourceWriter();

                handlerWriter.println("if (!GWT.isScript()) {");
                handlerWriter.println(GuitEntryPoint.class.getCanonicalName() + ".getLogger().initLog(\"VIEW " + binderContext.getLog()
                    + "\");");
                handlerWriter.println("}");

                handlerWriter.print("presenter." + methodName + "(");
                handlerWriter.print(bindingParametersString);
                handlerWriter.println(");");

                handlerWriter.println("if (!GWT.isScript()) {");
                handlerWriter.println(GuitEntryPoint.class.getCanonicalName() + ".getLogger().endLog();");
                handlerWriter.println("}");

                eventHandlerWriter.println(binderContext.build(handlerWriter));

                eventHandlerWriter.outdent();
                eventHandlerWriter.println("}");
                eventHandlerWriter.outdent();
                eventHandlerWriter.print("}");

                String addMethodName = getAddMethodName(eventName);
                if (bindingFields != null) {
                    writer.print("final " + handlerTypeName + " " + methodName + " =");
                    writer.print(eventHandlerWriter.toString());
                    writer.println(";");

                    for (String f : bindingFields) {
                        writer.println("bindings.add(view." + f + "." + addMethodName + "(" + methodName + "));");
                    }
                } else {
                    writer.print("bindings.add(view." + addMethodName + "(" + eventHandlerWriter.toString() + "));");
                }
            }
        }
    }

    private void printViewFieldBindings(SourceWriter writer, JClassType presenterType, String viewTypeName,
        HashMap<String, JType> validBindingFieldsType) throws UnableToCompleteException {

        Set<String> validBindingFields = validBindingFieldsType.keySet();

        JClassType viewAccesorType = getType(ViewAccesor.class.getCanonicalName());

        JField[] fields = presenterType.getFields();
        for (JField f : fields) {
            if (f.isAnnotationPresent(ViewField.class)) {
                String name = f.getName();
                String viewName = f.getAnnotation(ViewField.class).value();
                if (viewName.isEmpty()) {
                    viewName = name;
                }
                if (!validBindingFields.contains(viewName)) {
                    error("The field '%s' do not exists. Found: %s.%s", viewName, presenterType.getQualifiedSourceName(), name);
                }

                JClassType type = f.getType().isInterface();
                if (type == null) {
                    error("A ViewField must be an interface. Found: %s.%s", presenterType.getQualifiedSourceName(), name);
                }
                if (type.isAssignableTo(viewAccesorType)) {
                    if (!type.isAnnotationPresent(Implementation.class)) {
                        error("The ViewAccesor is not annotated with @Implementation. Found: %s", type.getQualifiedSourceName());
                    }

                    JClassType implementation = getType(type.getAnnotation(Implementation.class).value().getCanonicalName());

                    // If they are parameterized look for the base type
                    JParameterizedType implementationParameterized = implementation.isParameterized();
                    if (implementationParameterized != null) {
                        implementation = implementationParameterized.getBaseType();
                    }
                    JParameterizedType typeParameterized = type.isParameterized();
                    if (typeParameterized != null) {
                        type = typeParameterized.getBaseType();
                    }

                    // Verify that they are asignable
                    if (!implementation.isAssignableTo(type)) {
                        error("An implementation of a ViewAccesor must implement the ViewAccesor interface. Found: %s",
                            implementation.getQualifiedSourceName());
                    }
                    writer.println("{");
                    writer.indent();
                    writer.println(implementation.getQualifiedSourceName() + " accesor = new " + implementation.getQualifiedSourceName()
                        + "();");
                    writer.println("accesor.setTarget(view." + viewName + ");");
                    writer.println("presenter." + name + " = accesor;");
                    writer.outdent();
                    writer.print("}");
                    writer.println();
                } else if (type == null || type.isAssignableFrom(validBindingFieldsType.get(viewName).isClassOrInterface())) {
                    writer.println("presenter." + name + " = " + "(" + f.getType().getQualifiedSourceName() + ")" + "view." + viewName
                        + ";");
                    writer.println();
                } else {
                    // Interface emulation (without exceptions)
                    writer.println("presenter." + name + " = new " + type.getParameterizedQualifiedSourceName() + "() {");
                    writer.indent();

                    ArrayList<JMethod> methods = new ArrayList<JMethod>();
                    findAllMethods(type, methods);
                    for (JMethod m : methods) {
                        writer.print(m.getReadableDeclaration(false, true, true, true, true));
                        writer.println("{");
                        writer.indent();

                        // Find the parameters
                        StringBuilder callParameters = new StringBuilder();
                        for (JParameter p : m.getParameters()) {
                            if (callParameters.length() > 0) {
                                callParameters.append(", ");
                            }
                            if (p.isAnnotationPresent(ImplicitCast.class)) {
                                callParameters.append("(");
                                callParameters.append(p.getAnnotation(ImplicitCast.class).value().getCanonicalName());
                                callParameters.append(") ");
                            }
                            callParameters.append(p.getName());
                        }

                        JType returnType = m.getReturnType();
                        if (!returnType.equals(JPrimitiveType.VOID)) {
                            writer.print("return ");
                            
                            // Implicit cast
                            writer.print("(" + returnType.getParameterizedQualifiedSourceName() + ")");
                        }
                        
                        writer.indent();
                        writer.println(createdClassName + ".this.view." + viewName + "." + m.getName() + "(" + callParameters.toString()
                            + ");");
                        writer.outdent();

                        writer.outdent();
                        writer.println("}");
                        writer.println();
                    }

                    // Get .equals working on emulated interfaces for event.getSource() comparations
                    writer.println("@Override");
                    writer.println("public boolean equals(Object obj) {");
                    writer.indent();
                    writer.println("return view." + viewName + ".equals(obj);");
                    writer.outdent();
                    writer.println("}");
                    writer.println();

                    writer.outdent();
                    writer.println("};");
                }
            }
        }
    }

    @Override
    protected void processComposer(ClassSourceFileComposerFactory composer) {
        gwtEventType = getType(GwtEvent.class.getCanonicalName());
        gwtHandlerType = getType(EventHandler.class.getName());
        domEventsPackage = getPackage("com.google.gwt.event.dom.client");
        sharedEventsPackage = getPackage("com.google.gwt.event.logical.shared");
        composer.addImport(GWT.class.getCanonicalName());
        composer.addImplementedInterface(typeName);
    }

    protected BinderContextImpl processMethodContributors(JClassType presenterType, JClassType viewType, JClassType viewInterfaceType,
        String viewTypeName, JMethod method, JClassType eventType, String[] parameterGetters) throws UnableToCompleteException {
        BinderContextImpl binderContext = new BinderContextImpl(method, eventType, presenterType, viewType, viewInterfaceType,
            viewTypeName, parameterGetters);
        for (Entry<Class<? extends Annotation>, BinderContributor> entry : contributors.entrySet()) {
            if (method.isAnnotationPresent(entry.getKey())) {
                BinderContributor contributor = entry.getValue();
                binderContext.log(contributor.name());
                contributor.contribute(binderContext, logger, context);
            }
        }
        return binderContext;
    }

    protected void validateHandler(JMethod m, String name, String presenterName) throws UnableToCompleteException {
        if (m.isStatic()) {
            error("All event handlers must not be static. Found: %s.%s", presenterName, name);
        }

        if (m.isPrivate()) {
            error("All event handlers must not be private. Found: %s.%s", presenterName, name);
        }

        JPrimitiveType returnTypePrimitive = m.getReturnType().isPrimitive();
        if (returnTypePrimitive == null || !returnTypePrimitive.equals(JPrimitiveType.VOID)) {
            error("All event handlers should return void. Found: %s.%s", presenterName, name);
        }
    }
}
